package query

import (
	"fmt"

	"github.com/pingcap/errors"
	"github.com/romberli/das/config"
	"github.com/romberli/das/global"
	"github.com/romberli/das/internal/dependency/query"
	"github.com/romberli/go-util/common"
	"github.com/romberli/go-util/constant"
	"github.com/romberli/go-util/middleware"
	"github.com/romberli/go-util/middleware/clickhouse"
	"github.com/romberli/go-util/middleware/mysql"
	"github.com/romberli/log"
	"github.com/spf13/viper"
)

var (
	_ query.DASRepo     = (*DASRepo)(nil)
	_ query.MonitorRepo = (*MySQLRepo)(nil)
	_ query.MonitorRepo = (*ClickhouseRepo)(nil)
)

type DASRepo struct {
	Database middleware.Pool
}

// NewDASRepo returns query.DASRepo
func NewDASRepo(db middleware.Pool) query.DASRepo {
	return newDASRepo(db)
}

// NewDASRepoWithGlobal returns query.DASRepo with global mysql pool
func NewDASRepoWithGlobal() query.DASRepo {
	return newDASRepo(global.DASMySQLPool)
}

// NewDASRepo returns *DASRepo
func newDASRepo(db middleware.Pool) *DASRepo {
	return &DASRepo{Database: db}
}

// Execute executes given command and placeholders on the middleware
func (dr *DASRepo) Execute(command string, args ...interface{}) (middleware.Result, error) {
	conn, err := dr.Database.Get()
	if err != nil {
		return nil, err
	}
	defer func() {
		err = conn.Close()
		if err != nil {
			log.Errorf("query DASRepo.Execute(): close database connection failed. err: \n%+v", err)
		}
	}()

	return conn.Execute(command, args...)
}

// Transaction returns a middleware.Transaction that could execute multiple commands as a transaction
func (dr *DASRepo) Transaction() (middleware.Transaction, error) {
	return dr.Database.Transaction()
}

type MySQLRepo struct {
	config query.Config
	conn   *mysql.Conn
}

// NewMySQLRepo returns a new query.MonitorRepo
func NewMySQLRepo(config query.Config, conn *mysql.Conn) query.MonitorRepo {
	return newMySQLRepo(config, conn)
}

// NewMySQLRepo returns a new *MySQLRepo
func newMySQLRepo(config query.Config, conn *mysql.Conn) *MySQLRepo {
	return &MySQLRepo{
		config: config,
		conn:   conn,
	}
}

// getConfig gets Config
func (mr *MySQLRepo) getConfig() query.Config {
	return mr.config
}

// Close closes the connection
func (mr *MySQLRepo) Close() error {
	return mr.conn.Close()
}

// GetByServiceNames gets the queries by the service names of the mysql servers
func (mr *MySQLRepo) GetByServiceNames(serviceName []string) ([]query.Query, error) {
	interfaces, err := common.ConvertInterfaceToSliceInterface(serviceName)
	if err != nil {
		return nil, err
	}

	services, err := middleware.ConvertSliceToString(interfaces...)
	if err != nil {
		return nil, err
	}

	sql := fmt.Sprintf(mysqlQueryWithServiceNames, services)

	return mr.execute(sql,
		mr.getConfig().GetStartTime().Format(constant.DefaultTimeLayout),
		mr.getConfig().GetEndTime().Format(constant.DefaultTimeLayout),
		viper.GetInt(config.QueryMinRowsExaminedKey),
		viper.GetFloat64(config.QueryMinExecTimeKey),
		mr.getConfig().GetLimit(),
		mr.getConfig().GetOffset(),
	)
}

// GetByDBName gets the queries by the service name and db name of the mysql server
func (mr *MySQLRepo) GetByDBName(serviceName, dbName string) ([]query.Query, error) {
	interfaces, err := common.ConvertInterfaceToSliceInterface([]string{serviceName})
	if err != nil {
		return nil, err
	}

	services, err := middleware.ConvertSliceToString(interfaces...)
	if err != nil {
		return nil, err
	}

	sql := fmt.Sprintf(mysqlQueryWithDBName, services)

	return mr.execute(sql,
		dbName,
		mr.getConfig().GetStartTime().Format(constant.DefaultTimeLayout),
		mr.getConfig().GetEndTime().Format(constant.DefaultTimeLayout),
		viper.GetInt(config.QueryMinRowsExaminedKey),
		viper.GetFloat64(config.QueryMinExecTimeKey),
		mr.getConfig().GetLimit(),
		mr.getConfig().GetOffset())
}

// GetBySQLID return query.query by SQL ID
func (mr *MySQLRepo) GetBySQLID(serviceName, sqlID string) (query.Query, error) {
	interfaces, err := common.ConvertInterfaceToSliceInterface([]string{serviceName})
	if err != nil {
		return nil, err
	}

	services, err := middleware.ConvertSliceToString(interfaces...)
	if err != nil {
		return nil, err
	}

	sql := fmt.Sprintf(mysqlQueryWithSQLID, services)

	queries, err := mr.execute(sql,
		sqlID,
		mr.getConfig().GetStartTime().Format(constant.DefaultTimeLayout),
		mr.getConfig().GetEndTime().Format(constant.DefaultTimeLayout),
	)
	if len(queries) == constant.ZeroInt {
		return nil, errors.Errorf("sql not found. sql_id: %s, service_name: %s", sqlID, serviceName)
	}
	return queries[constant.ZeroInt], err
}

// execute executes the SQL with args
func (mr *MySQLRepo) execute(command string, args ...interface{}) ([]query.Query, error) {
	log.Debugf("query MySQLRepo.execute() sql: %s, args: %v", command, args)

	// get slow queries from the monitor database
	result, err := mr.conn.Execute(command, args...)
	if err != nil {
		return nil, err
	}
	// init queries
	queries := make([]query.Query, result.RowNumber())
	for i := constant.ZeroInt; i < result.RowNumber(); i++ {
		queries[i] = NewEmptyQuery()
	}
	// map result to queries
	err = result.MapToStructSlice(queries, constant.DefaultMiddlewareTag)
	if err != nil {
		return nil, err
	}

	return queries, nil
}

type ClickhouseRepo struct {
	config query.Config
	conn   *clickhouse.Conn
}

// NewClickHouseRepo returns a new query.MonitorRepo
func NewClickHouseRepo(config query.Config, conn *clickhouse.Conn) query.MonitorRepo {
	return newClickHouseRepo(config, conn)
}

// newClickHouseRepo returns a new *ClickHouseRepo
func newClickHouseRepo(config query.Config, conn *clickhouse.Conn) *ClickhouseRepo {
	return &ClickhouseRepo{
		config: config,
		conn:   conn,
	}
}

// getConfig returns the configuration
func (cr *ClickhouseRepo) getConfig() query.Config {
	return cr.config
}

// Close closes the connection
func (cr *ClickhouseRepo) Close() error {
	return cr.conn.Close()
}

// GetByServiceNames gets the queries by the service names of the mysql servers
func (cr *ClickhouseRepo) GetByServiceNames(serviceNames []string) ([]query.Query, error) {
	interfaces, err := common.ConvertInterfaceToSliceInterface(serviceNames)
	if err != nil {
		return nil, err
	}

	services, err := middleware.ConvertSliceToString(interfaces...)
	if err != nil {
		return nil, err
	}

	sql := fmt.Sprintf(clickhouseQueryWithServiceNames, services, services)

	return cr.execute(
		sql,
		cr.getConfig().GetStartTime(),
		cr.getConfig().GetEndTime(),
		viper.GetInt(config.QueryMinRowsExaminedKey),
		viper.GetFloat64(config.QueryMinExecTimeKey),
		cr.getConfig().GetLimit(),
		cr.getConfig().GetOffset(),
		cr.getConfig().GetStartTime(),
		cr.getConfig().GetEndTime(),
		viper.GetInt(config.QueryMinRowsExaminedKey),
		viper.GetFloat64(config.QueryMinExecTimeKey),
	)
}

// GetByDBName gets the queries by the service name and db name of the mysql server
func (cr *ClickhouseRepo) GetByDBName(serviceName, dbName string) ([]query.Query, error) {
	interfaces, err := common.ConvertInterfaceToSliceInterface([]string{serviceName})
	if err != nil {
		return nil, err
	}

	services, err := middleware.ConvertSliceToString(interfaces...)
	if err != nil {
		return nil, err
	}

	sql := fmt.Sprintf(clickhouseQueryWithDBName, services, services)

	return cr.execute(sql,
		dbName,
		dbName,
		cr.getConfig().GetStartTime(),
		cr.getConfig().GetEndTime(),
		viper.GetInt(config.QueryMinRowsExaminedKey),
		viper.GetFloat64(config.QueryMinExecTimeKey),
		cr.getConfig().GetLimit(),
		cr.getConfig().GetOffset(),
		dbName,
		dbName,
		cr.getConfig().GetStartTime(),
		cr.getConfig().GetEndTime(),
		viper.GetInt(config.QueryMinRowsExaminedKey),
		viper.GetFloat64(config.QueryMinExecTimeKey),
	)
}

// GetBySQLID returns query.Query by SQL ID
func (cr *ClickhouseRepo) GetBySQLID(serviceName, sqlID string) (query.Query, error) {
	interfaces, err := common.ConvertInterfaceToSliceInterface([]string{serviceName})
	if err != nil {
		return nil, err
	}

	services, err := middleware.ConvertSliceToString(interfaces...)
	if err != nil {
		return nil, err
	}

	sql := fmt.Sprintf(clickhouseQueryWithSQLID, services, services)

	queries, err := cr.execute(sql,
		sqlID,
		cr.getConfig().GetStartTime(),
		cr.getConfig().GetEndTime(),
		sqlID,
		cr.getConfig().GetStartTime(),
		cr.getConfig().GetEndTime(),
	)
	if len(queries) == constant.ZeroInt {
		return nil, errors.Errorf("sql not found. sql_id: %s, service_name: %s", sqlID, serviceName)
	}

	return queries[constant.ZeroInt], err
}

func (cr *ClickhouseRepo) execute(command string, args ...interface{}) ([]query.Query, error) {
	log.Debugf("query ClickhouseRepo.execute() sql: %s, args: %v", command, args)

	// get slow queries from the monitor database
	result, err := cr.conn.Execute(command, args...)
	if err != nil {
		return nil, err
	}
	// init queries
	queries := make([]query.Query, result.RowNumber())
	for i := constant.ZeroInt; i < result.RowNumber(); i++ {
		queries[i] = NewEmptyQuery()
	}
	// map result to queries
	err = result.MapToStructSlice(queries, constant.DefaultMiddlewareTag)
	if err != nil {
		return nil, err
	}

	return queries, nil
}
